/*
 *    Copyright 2008 Tim Jansen
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.actorsguildframework.internal;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

import org.actorsguildframework.Actor;
import org.actorsguildframework.AsyncResult;
import org.actorsguildframework.ConfigurationException;
import org.actorsguildframework.annotations.ConcurrencyModel;
import org.actorsguildframework.annotations.Initializer;
import org.actorsguildframework.annotations.Message;
import org.actorsguildframework.annotations.Model;

/**
 * Describes the configuration of an Actor sub-class.
 * Immutable thread-safe.
 * @see MessageImplDescriptor
 */
public final class ActorClassDescriptor {
	/**
	 * Bitset of all modifiers that are not allowed in actor classes.
	 */
	private final static int FORBIDDEN_CLASS_MODIFIERS = 
		Modifier.ABSTRACT | Modifier.FINAL | Modifier.INTERFACE; 
	
	/**
	 * The class of the actor.
	 */
	private final Class<? extends Actor> actorClass;

	/**
	 * The effective concurrency model of the message. 
	 */
	private final ConcurrencyModel concurrencyModel;
	
	/**
	 * The message implementations of the actor class.
	 * Warning: will only be created, but not be initialized in the constructor.
	 */
	private final MessageImplDescriptor[] messages;

	/**
	 * The initializers of the actor class.
	 * Warning: will only be created, but not be initialized in the constructor.
	 */
	private final MessageImplDescriptor[] initializers;
	
	/**
	 * Creates a new instance.
	 * @param actor the class of the actor
	 * @param concurrencyModel the concurrency model used by the actor
	 * @param initializerCount the number of initializers of the actor
	 * @param messageCount the number of messages of the actor
	 */
	private ActorClassDescriptor(Class<? extends Actor> actor, 
			ConcurrencyModel concurrencyModel, int initializerCount, int messageCount ) {
		this.actorClass = actor;
		this.concurrencyModel = concurrencyModel;
		this.initializers = new MessageImplDescriptor[initializerCount];
		this.messages = new MessageImplDescriptor[messageCount];
	}
	
	/**
	 * Creates a new ActorClassDescriptor for the given actor class.
	 * @param actorClass the actor's class
	 * @return the new instance
	 * @throws ConfigurationException if the agent is not configured correctly
	 */
	public static ActorClassDescriptor create(Class<? extends Actor> actorClass) {
		if ((actorClass.getModifiers() & FORBIDDEN_CLASS_MODIFIERS) != 0)
			throw new ConfigurationException(String.format("Actor class %s has illegal modifiers. It must not be final, abstract or an interface.", actorClass));

		if (Modifier.isPrivate(actorClass.getModifiers()))
			throw new ConfigurationException(String.format("Actor class %s must not be private.", actorClass));
				
		if ((actorClass.getDeclaringClass() != null) && !Modifier.isStatic(actorClass.getModifiers()))
			throw new ConfigurationException(String.format("Actor class %s is a non-static inner class. If you declare an actor as inner class, you must use the 'static' modifier.", actorClass));
		
		// check that there is a default constructor
		Constructor<?> ctor;
		try {
			ctor = actorClass.getDeclaredConstructor();
		}
		catch (NoSuchMethodException e)
		{
			throw new ConfigurationException(String.format("Actor class %s does not have a default constructor (without arguments). "+
					"A default constructor is required for every actor. If you need to initialize an Actor with arguments, create an @Initializer.", actorClass), 
					e);
		}
		
		if (Modifier.isPrivate(ctor.getModifiers()))
			throw new ConfigurationException(String.format("Default constructor of class %s must not be private. Every actor needs a public default constructor.", actorClass));
		
		// evaluate @Model
		Model ac = actorClass.getAnnotation(Model.class);
		ConcurrencyModel actorConcurrencyModel = (ac != null) ?  ac.value() : ConcurrencyModel.SingleThreaded;
		
		// find initializers and messages
		ArrayList<Method> initializers = new ArrayList<Method>();
		ArrayList<Method> messages = new ArrayList<Method>();
		for (Method method: actorClass.getDeclaredMethods())
			if (method.getAnnotation(Message.class) != null) {
				if (method.getAnnotation(Initializer.class) == null)
					messages.add(method);
				else
					throw new ConfigurationException(String.format("In Actor class %s, method %s is declared" +
							"as both @Message and @Initializer. A method can not be both.",
							actorClass, method));
			}
			else if (method.getAnnotation(Initializer.class) != null)
				initializers.add(method);
			else if (method.getReturnType().equals(AsyncResult.class) && 
					((method.getModifiers() & (Modifier.PRIVATE | Modifier.FINAL | Modifier.STATIC) ) == 0))
					throw new ConfigurationException(String.format("In Actor class %s, method %s returns AsyncResult, " +
							"but has no @Message or @Initializer annotation. \n" +
							"This is not allowed, in order to prevent methods that are accidentally not declared as messages. " +
							"Please note that you also need to specify @Message or @Initializer if you are overriding " +
							"an inherited method (Java annotations support the concept of inheritance only for classes, not " +
							"for methods). If you are really sure that your method should not be a message, you " +
							"must declare it either as 'private' or 'final'.",
							actorClass, method));

		// create the instances
		ActorClassDescriptor instance = new ActorClassDescriptor(actorClass, actorConcurrencyModel,
				initializers.size(), messages.size());
		for (int i = 0; i < initializers.size(); i++)
			instance.initializers[i] = MessageImplDescriptor.createInitializerDescriptor(instance, initializers.get(i));
		for (int i = 0; i < messages.size(); i++)
			instance.messages[i] = MessageImplDescriptor.createMessageDescriptor(instance, messages.get(i));
		
		return instance;
	}
	
	/**
	 * Returns the effective default concurrency model of the Actor. 
	 * @return the ConcurrencyMode
	 */
	public ConcurrencyModel getDefaultConcurrencyModel()
	{
		return concurrencyModel;
	}

	/**
	 * Returns true if the actor has any initializers.
	 * @return true if there are initializers, false otherwise
	 */
	public boolean hasInitializer() {
		return initializers.length > 0;
	}

	/**
	 * Returns the number of initializers.
	 * @return the number of initializers
	 */
	public int getInitializerCount() {
		return initializers.length;
	}

	/**
	 * Returns the initializer at the given index.
	 * @param index the index of the initializer
	 * @return the descriptor
	 * @throws IndexOutOfBoundsException if there is no initializer at the given index 
	 * @see #getInitializerCount()
	 */
	public MessageImplDescriptor getInitializer(int index) {
		return initializers[index];
	}

	/**
	 * Returns the message at the given index.
	 * @param index the index of the message
	 * @return the descriptor
	 * @throws IndexOutOfBoundsException if there is no message at the given index 
	 * @see #getMessageCount()
	 */
	public MessageImplDescriptor getMessage(int index) {
		return messages[index];
	}

	/**
	 * Returns the number of messages (not including initializers!).
	 * @return the number of messages
	 */
	public int getMessageCount() {
		return messages.length;
	}

	
	/**
	 * Returns the class of the actor implementation
	 * @return the implementation class
	 */
	public Class<? extends Actor> getActorClass() {
		return actorClass;
	}
}
